from tkinter import *
from tkinter import ttk, font, filedialog as fd
import shutil, os, ctypes, pyglet
from fractions import Fraction
from PIL import ImageTk, Image, ImageOps, ImageDraw, ImageFont
from Ingredient import Ingredient
from Recipe import Recipe
from In_refrigerator import In_refrigerator
from In_cabinet import In_cabinet
from MyPantry import MyPantry
from Edit_ingredient import Edit_ingredient
from Recipes_frame import Recipes_frame
from Side_frame import Side_frame
from In_refrigerator_frame import In_refrigerator_frame
from In_cabinet_frame import In_cabinet_frame
from Edit_and_add_ingredients import Edit_and_add_ingredients
from Edit_and_add_recipes import Edit_and_add_recipes
from Edit_recipe_instructions_frame import Edit_recipe_instructions_frame
from View_recipe_frame import View_recipe_frame


class Program:
    """
    Purpose:
        Main class of the program. Used as a central place for functions and variables
    Instance variables:
        Program.min_title_font_size: Setting that controls the smallest
            size that labels using the title font can be from resizing window
        Program.max_title_font_size: Setting that controls the largets
            size that labels using the title font can be from resizing window
        Program.title_font_cache: dictionary that is used to find which title font size to
            use at specific window size
        Program.min_pantry_font_size: Setting that controls the smallest
            size that labels using the pantry font can be from resizing window
        Program.max_pantry_font_size: Setting that controls the largets
            size that labels using the pantry font can be from resizing window
        Program.pantry_font_cache: dictionary that is used to find which title font size to
            use at specific window size
        Program.controller: main root tk window
        Program.prev_width: saves the window width to prevent running function
            to resize window if the window size has not changed
        Program.prev_height: saves the window height to prevent running function
            to resize window if the window size has not changed
        Program.roboto_font: actual font to use for the title and pantry fonts
    Methods:
        save: saves recipes, ingredients, and/or pantry depending
            on parameters provided
        load: loads ingredients, recipes, and pantry from save files
        save_ingredients: takes all know ingredients and saves them to a csv file
        save_recipes: takes all know recipes and saves them to a csv file
        save_pantry: saves ingredients that are in your cabinet and refrigerator
        load_ingredients: opens a save file and creates ingredient objects for
            every ingredient
        load_recipes: opens a save file and creates recipe objects for
            every recipe
        load_pantry: opens a save file and updates the ingredients in your
            cabinet and refrigerator
        add_ingredient: creates an ingredient object with the provided parameters
        add_recipe: creates a recipe object with the provided parameters
        search_ingredient_name: returns the ingredient object from the provided
            name or None if the ingredient cant be found
        search_recipe_name: returns the recipe object from the provided
            name or None if the recipe cant be found
        search_recipes_by_ingredients: takes a set of ingredient names and finds
            recipes that use the ingredients and also missing ingredients that
            are also needed in the recipe
        only_allow_num_and_equations: checks if the character typed is in
            either floats or equations, and if it isnt it stops the character
            from being typed
        only_allow_int: checks if the character typed is a number and if it
            isnt it stops the character from being typed
        convert_string_to_float: takes a string of an equation and
            simplifies/solves the equation, returning a float
        convert_float_to_string: takes a float and returns a
            string of it but in a fraction
        update_due_to_item_added: updates required assests of the gui
            that would change due to a ingredient being added
        update_pantry: refreshes labels in pantry that may have changed
        remove_ingredient_from_recipe: removes the ingredient from a recipe
        delete_ingredient: deletes an ingredient from the program
        remove_recipe_from_ingredient: removes a recipe from an ingredient
        delete_recipe: deletes a recipe from the program
        rename_recipe: changes the name of a recipe in the program
        resize_screen: updates the size of everthing that is currently
            displayed on the screen due to the screen resizing
        set_font_size: finds and changes the font size to fit the window screen
        convert_rgb_to_hex: changes an rgb tuple into a hex string
        convert_hex_to_rgb: changes a hex string into an rgb tuple
        modify_rgb: offests an rgb tuple by the provided rgb offset
        overlay_colors: finds the rgb color of a color with another
            colored overlayed with a percentage of opacity
        create_rounded_button_image: creates a round button image with
            specific text and that is a specific color
        change_program_colors: updates the ui color scheme
    """

    min_title_font_size = 10
    max_title_font_size = 20
    title_font_cache = {}
    min_pantry_font_size = 10
    max_pantry_font_size = 15
    pantry_font_cache = {}
    light_mode = True
    controller = None
    prev_width = None
    prev_height = None
    roboto_font = None

    def save(recipe_save_file=None, ingredients_save_file=None, pantry_save_file=None):
        if ingredients_save_file != None:
            Program.save_ingredients(ingredients_save_file)
        if recipe_save_file != None:
            Program.save_recipes(recipe_save_file)
        if pantry_save_file != None:
            Program.save_pantry(pantry_save_file)

    def load(recipe_load_file, ingredients_load_file, pantry_load_file):
        try:
            Program.load_ingredients(ingredients_load_file)
        except:
            pass
        try:
            Program.load_recipes(recipe_load_file)
        except:
            pass
        try:
            Program.load_pantry(pantry_load_file)
        except:
            pass

    def save_ingredients(save_file_name):
        with open(save_file_name, "w") as save_file:
            save_file.write("Ingredient Name,Cost,Unit,Recipes That Use Ingredient\n")
            for ingredient_object in tuple(Ingredient.ingredients_dict.values()):
                save_file.write(str(ingredient_object))

    def save_recipes(save_file_name):
        with open(save_file_name, "w") as save_file:
            save_file.write(
                "Recipe Name,Set of Ingredients,Recipe Instructions,Cost of Recipe\n"
            )
            for recipe_object in tuple(Recipe.recipes_dict.values()):
                save_file.write(str(recipe_object))

    def save_pantry(save_file_name):
        with open(save_file_name, "w") as save_file:
            save_file.write("In Refrigerator\n")
            save_file.write("\n".join(In_refrigerator.refrigerator_list) + "\n")
            save_file.write("In Cabinet\n")
            save_file.write("\n".join(In_cabinet.cabinet_list))

    def load_ingredients(load_file_name):
        with open(load_file_name) as load_file:
            load_file.readline()
            list_of_data = []
            for line in load_file:
                line = line.strip()
                list_of_data = []
                current_quotes = ""
                temp_string = ""
                for char in line:
                    if char == '"':
                        if current_quotes == "":
                            current_quotes = '"'
                        else:
                            current_quotes = ""
                        temp_string = temp_string + char
                    elif char == ",":
                        if current_quotes == "":
                            list_of_data.append(temp_string)
                            temp_string = ""
                        else:
                            temp_string = temp_string + char
                    else:
                        temp_string = temp_string + char
                list_of_data.append(temp_string)
                list_of_data = [
                    string[1:-1].replace('""', '"') if string[0] == '"' else string
                    for string in list_of_data
                ]
                set_of_recipes = set()
                current_double_quotes = ""
                current_single_quotes = ""
                temp_string = ""
                if list_of_data[3] != "set()":
                    for char in list_of_data[3][1:-1]:
                        if char == '"':
                            if current_double_quotes == "":
                                current_double_quotes = '"'
                            else:
                                current_double_quotes = ""
                        elif char == "'":
                            if (
                                current_single_quotes == ""
                                and current_double_quotes == ""
                            ):
                                current_single_quotes = "'"
                            else:
                                current_single_quotes = ""
                        elif char == ",":
                            if (
                                current_double_quotes == ""
                                and current_single_quotes == ""
                            ):
                                set_of_recipes.add(temp_string)
                                temp_string = ""
                            else:
                                temp_string = temp_string + char
                        elif current_double_quotes != "" or current_single_quotes != "":
                            temp_string = temp_string + char
                    set_of_recipes.add(temp_string)

                if len(list_of_data) > 0:
                    Ingredient(
                        list_of_data[0],
                        float(list_of_data[1]),
                        list_of_data[2],
                        set_of_recipes,
                    )

    def load_recipes(load_file_name):
        with open(load_file_name) as load_file:
            load_file.readline()
            list_of_data = []
            for line in load_file:
                while line.count('"') % 2 == 1:
                    line = line + load_file.readline()
                line = line.strip()
                list_of_data = []
                current_quotes = ""
                temp_string = ""
                for char in line:
                    if char == '"':
                        if current_quotes == "":
                            current_quotes = '"'
                        else:
                            current_quotes = ""
                        temp_string = temp_string + char
                    elif char == ",":
                        if current_quotes == "":
                            list_of_data.append(temp_string)
                            temp_string = ""
                        else:
                            temp_string = temp_string + char
                    else:
                        temp_string = temp_string + char
                list_of_data.append(temp_string)
                list_of_data = [
                    string[1:-1].replace('""', '"')
                    if len(string) > 0 and string[0] == '"'
                    else string
                    for string in list_of_data
                ]
                if len(list_of_data) > 0:
                    comma = 0
                    list_for_dict = []
                    temp_string = ""
                    for char in list_of_data[1][1:-1]:
                        if char == ",":
                            if comma % 2 == 1:
                                comma += 1
                                list_for_dict.append(temp_string)
                                temp_string = ""
                            else:
                                comma += 1
                                temp_string = temp_string + char
                        else:
                            temp_string = temp_string + char
                    list_for_dict.append(temp_string)
                    ingredients_dict = {}
                    if list_for_dict == [""]:
                        list_for_dict.clear()
                    for item in list_for_dict:
                        item = item.strip()
                        open_bracket_location = item.rfind("[")
                        ingredients_dict[item[1 : open_bracket_location - 3]] = [
                            float(
                                item[
                                    open_bracket_location
                                    + 1 : item.find(",", open_bracket_location)
                                ]
                            ),
                            item[item.find(" ", open_bracket_location) + 2 : -2],
                        ]

                    Recipe(
                        list_of_data[0],
                        ingredients_dict,
                        list_of_data[2],
                        float(list_of_data[3]),
                    )

    def load_pantry(load_file_name):
        with open(load_file_name) as load_file:
            load_file.readline()
            refrigerator_list = []
            cabinet_list = []
            current_list = refrigerator_list
            for line in load_file:
                line = line.strip()
                if line == "In Cabinet":
                    current_list = cabinet_list
                elif len(line) > 0:
                    current_list.append(line)
            In_refrigerator(refrigerator_list)
            In_cabinet(cabinet_list)

    def add_ingredient(name, cost, unit):
        Ingredient(name, cost, unit)

    def add_recipe(name, ingredients, instructions):
        Recipe(name, ingredients, instructions)

    def search_ingredient_name(name):
        return Ingredient.ingredients_dict.get(name)

    def search_recipe_name(name):
        return Recipe.recipes_dict.get(name)

    def search_recipes_by_ingredients(ingredients_to_search_set):
        missing_ingredients_dict = {}
        sorted_recipes_dict = {}
        for ingredient in tuple(ingredients_to_search_set):
            for recipe in Ingredient.ingredients_dict[ingredient].recipes:
                if recipe not in missing_ingredients_dict:
                    missing_ingredients_set = (
                        set(Recipe.recipes_dict[recipe].ingredients)
                        - ingredients_to_search_set
                    )
                    missing_ingredients_dict.setdefault(recipe, missing_ingredients_set)
                    sorted_recipes_dict.setdefault(len(missing_ingredients_set), [])
                    sorted_recipes_dict[len(missing_ingredients_set)].append(recipe)
        sorted_recipes_list = []
        for num in sorted(sorted_recipes_dict.keys()):
            sorted_recipes_list = sorted_recipes_list + sorted(sorted_recipes_dict[num])

        return [sorted_recipes_list, missing_ingredients_dict]

    def only_allow_num_and_equations(event):
        if not (
            event.char == "."
            or event.char.isnumeric()
            or event.char == "\x08"
            or event.keysym == "Left"
            or event.keysym == "Right"
            or event.char == "/"
            or event.char == " "
            or event.char == "+"
            or event.char == "-"
            or event.char == "*"
        ):
            return "break"

    def only_allow_int(event):
        if not (
            event.char.isnumeric()
            or event.char == "\x08"
            or event.keysym == "Left"
            or event.keysym == "Right"
        ):
            return "break"

    def convert_string_to_float(string):
        indx_dict = {}
        symbol_locations = []
        string = string.replace(" ", "+")
        if string == "":
            return 0
        elif string.isnumeric():
            return float(string)
        symbols = ("*", "/", "+", "-")

        for indx, char in enumerate(string):
            if char in symbols:
                indx_dict.setdefault(char, indx)
                symbol_locations.append(indx)
        for symbol in symbols:
            indx_dict.setdefault(symbol, -1)
        if len(symbol_locations) == 1:
            symbol = tuple(indx_dict.keys())[0]
            left_num, right_num = string.split(symbol)
            deci_indx = left_num.find(".")
            left_num = left_num[: deci_indx + 1] + left_num[deci_indx + 1 :].replace(
                ".", ""
            )
            deci_indx = right_num.find(".")
            right_num = right_num[: deci_indx + 1] + right_num[deci_indx + 1 :].replace(
                ".", ""
            )

            if symbol == "+":
                return float("0" + left_num) + float("0" + right_num)
            elif symbol == "-":
                return float("0" + left_num) - float("0" + right_num)
            elif symbol == "*":
                return float("0" + left_num) * float("0" + right_num)
            elif symbol == "/":
                if float("0" + right_num) == 0:
                    return 0
                else:
                    return float("0" + left_num) / float("0" + right_num)
        elif len(symbol_locations) == 0:
            deci_indx = string.find(".")
            string = string[: deci_indx + 1] + string[deci_indx + 1 :].replace(".", "")
            return float("0" + string)

        symbol_locations.append(-1)
        symbol_locations.append(len(string))
        if indx_dict["*"] != -1 or indx_dict["/"] != -1:
            if (indx_dict["*"] < indx_dict["/"] or indx_dict["/"] == -1) and indx_dict[
                "*"
            ] != -1:
                lower_split_location = max(
                    indx for indx in symbol_locations if indx < indx_dict["*"]
                )
                upper_split_location = min(
                    indx for indx in symbol_locations if indx > indx_dict["*"]
                )
            else:
                lower_split_location = max(
                    indx for indx in symbol_locations if indx < indx_dict["/"]
                )
                upper_split_location = min(
                    indx for indx in symbol_locations if indx > indx_dict["/"]
                )
        elif indx_dict["+"] != -1 or indx_dict["-"] != -1:
            if (indx_dict["+"] < indx_dict["-"] or indx_dict["-"] == -1) and indx_dict[
                "+"
            ] != -1:
                lower_split_location = max(
                    indx for indx in symbol_locations if indx < indx_dict["+"]
                )
                upper_split_location = min(
                    indx for indx in symbol_locations if indx > indx_dict["+"]
                )
            else:
                lower_split_location = max(
                    indx for indx in symbol_locations if indx < indx_dict["-"]
                )
                upper_split_location = min(
                    indx for indx in symbol_locations if indx > indx_dict["-"]
                )
        return Program.convert_string_to_float(
            string[: lower_split_location + 1]
            + str(
                Program.convert_string_to_float(
                    string[lower_split_location + 1 : upper_split_location]
                )
            )
            + string[upper_split_location:]
        )

    def convert_float_to_string(flt):
        final_string = ""
        whole_num, decimal = str(flt).split(".")
        frac = str(Fraction(float("." + decimal)).limit_denominator(50000))
        if whole_num != "0":
            final_string = final_string + whole_num
        if frac != "0":
            if whole_num != "0":
                final_string = final_string + " "
            final_string = final_string + frac
        return final_string

    def update_due_to_item_added():
        Program.update_pantry()
        Program.controller.frames[MyPantry].update_combo_box_ingredients()

    def update_pantry():
        Program.controller.frames[In_refrigerator_frame].update_remove_labels()
        Program.controller.frames[In_refrigerator_frame].update_add_labels()
        Program.controller.frames[In_cabinet_frame].update_remove_labels()
        Program.controller.frames[In_cabinet_frame].update_add_labels()
        Program.controller.frames[MyPantry].update_labels()
        Program.controller.frames[Recipes_frame].refresh_window()
        Program.save(pantry_save_file="pantry.csv")

    def remove_ingredient_from_recipe(ingredient_name, recipe_name):
        recipe = Program.search_recipe_name(recipe_name)
        del recipe.ingredients[ingredient_name]

    def delete_ingredient(ingredient_name):
        ingredient = Program.search_ingredient_name(ingredient_name)
        ingredient.cost = 0
        ingredient.update_recipes_cost()
        for recipe in tuple(Program.search_ingredient_name(ingredient_name).recipes):
            Program.remove_ingredient_from_recipe(ingredient_name, recipe)
        del Ingredient.ingredients_dict[ingredient_name]
        In_refrigerator.remove_ingrdient(ingredient_name)
        In_cabinet.remove_ingrdient(ingredient_name)

    def remove_recipe_from_ingredient(recipe_name, ingredient_name):
        ingredient = Program.search_ingredient_name(ingredient_name)
        ingredient.recipes.discard(recipe_name)

    def delete_recipe(recipe_name):
        recipe = Program.search_recipe_name(recipe_name)
        for ingredient in tuple(recipe.ingredients.keys()):
            Program.remove_recipe_from_ingredient(recipe_name, ingredient)
        del Recipe.recipes_dict[recipe_name]

    def rename_recipe(current_name, new_name):
        recipe = Program.search_recipe_name(current_name)
        Program.add_recipe(new_name, recipe.ingredients, recipe.instructions)
        path_to_image = f"{os.getcwd()}\\images\\{current_name}.png"
        if os.path.exists(path_to_image):
            os.rename(path_to_image, f"{os.getcwd()}\\images\\{new_name}.png")
        Program.delete_recipe(current_name)

    def resize_screen(force_update=False):

        if (
            Program.prev_width == None
            or Program.controller.winfo_width() != Program.prev_width
            or Program.controller.winfo_height() != Program.prev_height
            or force_update
        ):
            Program.prev_width = Program.controller.winfo_width()
            Program.prev_height = Program.controller.winfo_height()
            if Program.controller.frames[MyPantry] in Program.controller.current_frames:
                Program.set_font_size()
                Program.controller.frames[MyPantry].update()
                Program.controller.frames[MyPantry].update_labels()
            elif (
                Program.controller.frames[Recipes_frame]
                in Program.controller.current_frames
            ):
                Program.set_font_size()
                Program.controller.frames[Recipes_frame].update()
                Program.controller.frames[Recipes_frame].set_wordwrap()
                Program.controller.frames[Recipes_frame].set_arrows_images()
            elif (
                Program.controller.frames[View_recipe_frame]
                in Program.controller.current_frames
            ):
                Program.set_font_size()
                Program.controller.frames[View_recipe_frame].update()
                Program.controller.frames[View_recipe_frame].resize_image()
                Program.controller.frames[
                    View_recipe_frame
                ].update_instructions_word_wrap()
                Program.controller.frames[View_recipe_frame].update()
                Program.controller.frames[
                    View_recipe_frame
                ].ingredients_list_canvas.config(
                    scrollregion=Program.controller.frames[
                        View_recipe_frame
                    ].ingredients_list_canvas.bbox("all")
                )
                Program.controller.frames[
                    View_recipe_frame
                ].instructions_text_canvas.config(
                    scrollregion=Program.controller.frames[
                        View_recipe_frame
                    ].instructions_text_canvas.bbox("all")
                )
            elif (
                Program.controller.frames[In_refrigerator_frame]
                in Program.controller.current_frames
            ):
                Program.set_font_size()
            elif (
                Program.controller.frames[In_cabinet_frame]
                in Program.controller.current_frames
            ):
                Program.set_font_size()

    def set_font_size():
        if Program.title_font_cache == {}:
            interval = (
                Program.controller.winfo_screenwidth()
                - Program.controller.minsize()[0]
                + 2
            ) / (Program.max_title_font_size - Program.min_title_font_size + 1)
            for pos, size in enumerate(
                range(Program.min_title_font_size, Program.max_title_font_size + 1)
            ):
                Program.title_font_cache[
                    int((Program.controller.minsize()[0] - 1) + (pos + 1) * interval)
                ] = size

            interval = (
                Program.controller.winfo_screenwidth()
                - Program.controller.minsize()[0]
                + 2
            ) / (Program.max_pantry_font_size - Program.min_pantry_font_size + 1)
            for pos, size in enumerate(
                range(Program.min_pantry_font_size, Program.max_pantry_font_size + 1)
            ):
                Program.pantry_font_cache[
                    int((Program.controller.minsize()[0] - 1) + (pos + 1) * interval)
                ] = size
            Program.title_font_intervals_list = sorted(
                list(Program.title_font_cache.keys())
            )
            Program.pantry_font_intervals_list = sorted(
                list(Program.pantry_font_cache.keys())
            )
        controller_width = Program.controller.winfo_width()
        for maxwidth in Program.title_font_intervals_list:
            if maxwidth >= controller_width:
                Program.resizing_title_font["size"] = Program.title_font_cache[maxwidth]
                break

        for maxwidth in Program.pantry_font_intervals_list:
            if maxwidth >= controller_width:
                Program.resizing_pantry_font["size"] = Program.pantry_font_cache[
                    maxwidth
                ]
                break

    def convert_rgb_to_hex(rgb_tople):
        return "#%02x%02x%02x" % rgb_tople

    def convert_hex_to_rgb(hex_value):
        hex_value = hex_value.lstrip("#")
        return tuple(int(hex_value[i : i + 2], 16) for i in (0, 2, 4))

    def modify_rgb(main_rgb_tuple, change_rgb_tuple):
        new_rgb_tuple = (
            main_rgb_tuple[0] + change_rgb_tuple[0],
            main_rgb_tuple[1] + change_rgb_tuple[1],
            main_rgb_tuple[2] + change_rgb_tuple[2],
        )
        temp_list = []
        for indx, value in enumerate(new_rgb_tuple):
            if value > 255:
                value = 255
            elif value < 0:
                value = 0
            temp_list.append(value)
        return tuple(temp_list)

    def overlay_colors(rgb_base_tuple, rgb_overlay_tuple, overlay_opacity_percentage):
        new_rgb_tuple = Program.modify_rgb(
            tuple(
                map(
                    lambda x: x * (1 - overlay_opacity_percentage),
                    rgb_base_tuple,
                )
            ),
            tuple(map(lambda x: x * overlay_opacity_percentage, rgb_overlay_tuple)),
        )
        return tuple(map(round, new_rgb_tuple))

    def create_rounded_button_image(
        bg_hex, button_color_hex, button_text_hex, text, width, height, radius
    ):
        button_image = Image.new(
            "RGBA",
            (
                width,
                height,
            ),
            bg_hex + "FF",
        )
        button_draw = ImageDraw.Draw(button_image)
        button_draw.rounded_rectangle(
            (
                0,
                0,
                width,
                height,
            ),
            fill=button_color_hex + "FF",
            radius=22,
        )

        _, _, w, h = button_draw.textbbox((0, 0), text, font=Program.roboto_font)
        button_draw.text(
            (
                (width - w) / 2,
                (height - h) / 2,
            ),
            text,
            font=Program.roboto_font,
            fill=button_text_hex,
        )
        return ImageTk.PhotoImage(button_image)

    def change_program_colors(
        bg_color, primary_color, primary_variant, secondary_color
    ):
        Program.controller.frames[MyPantry].change_colors(
            bg_color, primary_color, primary_variant
        )
        Program.controller.frames[Recipes_frame].change_colors(
            bg_color, primary_color, primary_variant, secondary_color
        )
        Program.controller.frames[View_recipe_frame].change_colors(
            bg_color, primary_color, primary_variant, secondary_color
        )
        Program.controller.frames[Side_frame].change_colors(
            bg_color, primary_color, primary_variant, secondary_color
        )
        Program.controller.frames[Edit_and_add_ingredients].change_colors(
            bg_color, primary_color, primary_variant, secondary_color
        )
        Program.controller.frames[Edit_and_add_recipes].change_colors(
            bg_color, primary_color, primary_variant, secondary_color
        )
        Program.controller.frames[Edit_ingredient].change_colors(
            bg_color, primary_color, primary_variant, secondary_color
        )
        Program.controller.frames[In_refrigerator_frame].change_colors(
            bg_color, primary_color, primary_variant, secondary_color
        )
        Program.controller.frames[In_cabinet_frame].change_colors(
            bg_color, primary_color, primary_variant, secondary_color
        )
        Program.controller.frames[Edit_recipe_instructions_frame].change_colors(
            bg_color, primary_color, primary_variant, secondary_color
        )
        with open("gui\\color settings.csv", "w") as color_settings:
            color_settings.write("light mode, primary color, secondary color\n")
            color_settings.write(
                ",".join([str(Program.light_mode), primary_color, secondary_color])
            )
